Repository navigation
feat(cli,service-settings): os secret rewrap, the at-rest re-wrap of version-1 sys_secret ciphertext under each holder's producer scope (ADR-0128 §4.2, stage 2) - #21469
Conversation
…der's own reading of a stored ciphertext's AAD derivation (ADR-0128 §4.2) The at-rest re-wrap must tell a version-1 row from one already sealed under the current derivation without opening it, and must not restate the marker grammar that decrypt dispatches on. The reading is the provider's own readCiphertext, published from the package root. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…ret ciphertext (ADR-0128 §4.2) Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…fail-closed and verify-before-write (ADR-0128 §4.2) Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…ined in the re-wrap pins Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…ADR-0128 §4.2) Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
… its key before the boot The settings service registers sys_setting, the settings family's holder, so the re-wrap boots it as os secret orphans does. That service's own provider may mint a key in a development posture, so the re-wrap resolves its provider first, in the strict posture that never mints. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…wn provider, so no provider in the run mints a key; join the bootSchemaStack families Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…for the re-wrap Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 32 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 78cceb50b14774215fa5a62bec35c4079566bd4b && git checkout 78cceb50b14774215fa5a62bec35c4079566bd4b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin aa4632235ba571ef800b95e6bc18d00a30aa1d57 219457c0298cbd7a8a88a42839df952da7554b67 && git checkout -B drift-repro aa4632235ba571ef800b95e6bc18d00a30aa1d57 && git merge --no-ff 219457c0298cbd7a8a88a42839df952da7554b67
node scripts/docs-audit/affected-docs.mjs --json aa4632235ba571ef800b95e6bc18d00a30aa1d57
|
Contract reviewServed-tier: Scope read: card #21326 (body and every comment: triage ① Derived judgmentsADR-0128 §4.2, resumable: right. All state is in the rows. Safe against a live deployment: right, and real on every driver the command can reach. The only write is one Fails closed: right; no partial write is possible. A row whose stored fields do not form a handle, or that does not open under its attributed context, is Verify before write: right. The dry run: right. It is the default; Per-row scope attribution (D3): right, in this order. The ciphertext census: right. Every non-test Key handling: right; the command never mints in any posture, and no key is a refusal. The provider is built with The new published export Output: right, with one class named. The report is classes and counts: Every accept-set and public-surface change the diff implies, each judged:
② Semver levelThe levels are right; the The changeset The PR body's second line and the changeset both read The gates' verdicts do not rescue it: ③ Boundary flagsThe dev's deviations, each answered:
The dev's out-of-scope findings, each answered or escalated:
Further flags from this review:
Implemented-by: VERDICT: FAIL Failed item: ② the Generated by Claude Code |
The diff grows two published surfaces: a root export on @objectstack/service-settings (ciphertextDerivationStatus and its type) and the os secret rewrap command on @objectstack/cli. The levels (minor, minor) already satisfy a widening declaration. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Scope read: a re-review of PR #21469 after record ① Derived judgmentsThe delta touches nothing ① judged, so the previous record's ① holds at this head by reference. ② Semver levelRight at this head; the one failed item of
③ Boundary flags
Implemented-by: VERDICT: PASS The one failed item of Generated by Claude Code |
…throw oclif's exit signal, so a completed --json run prints one document and exits 0 (objectstack-ai#21495) Fixes objectstack-ai#21434 Clause-②: no ## What was wrong `this.exit(n)` throws oclif's exit signal (`code: 'EEXIT'`, `oclif.exit: n`). When it runs inside a `try`, the `try`'s own `catch` sees the signal first. Four migrate commands had a `catch` that reported whatever it caught, so the signal came back out as an error. Measured at the public door on base `aa4632235` (CLI run from source through `bin/run-dev.js`, a sqlite file with one `sys_metadata_history` row whose `recorded_by` is `system`): | command | before (`aa4632235`) | after (`34a0cd121e`) | |---|---|---| | `os migrate recorded-by --apply --yes --json` | row converted; result document, then `{"error":"EEXIT: 0","duration":1020}`; **exit 1** | row converted; one document (`JSON.parse` of the whole stdout succeeds); **exit 0** | | `os migrate resume --run RUN_ID --json` (run already concluded) | refusal-shaped document, then `{"error":"EEXIT: 0",…}`; **exit 1** | one document; **exit 0** | | `os migrate resume --run RUN_ID --yes --json` (journal row `run_done` removed) | "no loaded package registers" document, then `{"error":"EEXIT: 1",…}`; exit 1 | one document; exit 1 | The merge of `main` after `34a0cd121e` touched none of the four command files. ## The fix The existing idiom, `if (isExitSignal(error)) throw error;` (`packages/cli/src/utils/format.ts`), as the first statement of the swallowing `catch` in: - `migrate/recorded-by.ts`. 5 `this.exit` sites in the `try`, the completed-apply `this.exit(0)` among them. - `migrate/resume.ts`. 8 sites: resumed run, already-concluded run, unknown run id, plan not loaded, confirmation required, failed run. - `migrate/account-issuer.ts`. 2 sites: the refused pre-flight on the JSON face (second document `{"error":"EEXIT: 1"}`) and on the text face (extra `EEXIT: 1` line). - `migrate/apply.ts`. 2 sites, text face only: the `sys_account.issuer` pre-flight refusals. Its JSON face returns before them. There is no second helper and no producer elsewhere. The swallowing happens in each command's own `catch`, and nothing outside `packages/cli/src/commands/migrate/` changes at runtime. ## Census: every command that takes a JSON face, at `88ae5769c6` Derived from the oclif declarations. Each module under `src/commands` (the `src/` twin of `oclif.commands` = pattern, `./dist/commands`, `**/*.js`) is imported, and its default export's `static flags` is read, inherited flags included. A member declares a boolean `json` flag (32 commands) or a flag whose `options` include `'json'` (`--format json`, 14 commands). That makes 46 commands with 105 `this.exit`-in-`try` sites. - **In-family (4, fixed here):** `migrate recorded-by` (5 leaking sites), `migrate resume` (8), `migrate account-issuer` (2), `migrate apply` (2, text face). - **Already correct (14):** `cloud whoami` (1 site), `compile` (21), `build` (inherits `compile`'s 21), `environments bind` (2), `environments create` (1), `migrate audit-metadata-bodies` (2), `migrate files-to-references` (2), `migrate multi-value-columns` (4), `migrate summary-nulls` (2), `migrate value-shapes` (3), `secret orphans` (7), `secret rewrap` (6), `validate` (15), `verify` (1). - **Not affected, no `this.exit` inside a `try` (28):** `cloud login`, `cloud logout`, `data create`, `data delete`, `data get`, `data query`, `data update`, `diff`, `environments list`, `environments show`, `explain`, `i18n check`, `i18n extract`, `info`, `lint`, `login`, `logout`, `meta delete`, `meta get`, `meta list`, `meta register`, `meta resync`, `migrate`, `migrate meta`, `migrate plan`, `register`, `storage orphans`, `whoami`. **`secret rewrap`, which landed in objectstack-ai#21469 while this branch was open, is already correct.** Its 6 sites sit in the `try` at `rewrap.ts` line 195, whose `catch` (line 310) opens with the rethrow. objectstack-ai#21469 added that line, so nothing here edits `rewrap.ts`. This PR adds no driven case for it: it is not in-family, and the structural half covers its exit path. `json-stdout-purity.e2e.test.ts` is not touched. `this.error(…)` also raises a signal `isExitSignal` recognises. Measured: no JSON-capable command calls it inside a `try`. The only `this.error` calls inside a `try` are 2 in `init.ts`, which has no JSON face. ## The enumeration pin: `packages/cli/test/json-exit-signal.pin.test.ts` (unit tier) 1. **Analyzer fixtures (15 cases).** The detector is shown to fail on each shape it must catch: no rethrow, a catch with no binding, the rethrow not first, a second helper, `isExitSignal` imported from somewhere other than `utils/format.js`, an exit through a same-class helper or an arrow-function property, nested tries, an exit in an inner catch, an exit in a callback. It also passes the idiom, an unconditional rethrow, and try/finally. 2. **Structural, over the whole discovered population (46 cases + 3 meta).** Every `this.exit` inside a `try`, direct or through a same-class method, must have the `isExitSignal` rethrow as the first statement of every enclosing `catch`. The check covers the command's own source and its superclasses' sources. The meta checks are: package.json's oclif command strategy is the one the walk mirrors; every walked module is a command; the population floor (46) and site floor (105); the named anchors; and that `os build`'s chain reaches `compile.ts`. 3. **Driven, in-family completed paths (13 cases).** `recorded-by`, `resume` and `account-issuer` run in-process through oclif with a preloaded `Config`. `bootSchemaStack`, the journal runner, the sentinel scan and the collision probe are replaced through `vi.mock`. Each case asserts one JSON document on stdout (a bare `JSON.parse` of the whole of it) and the exit status. The cases are recorded-by `--apply --yes` completed → 0, compensated → 1, failed → 1, and the confirmation refusal → 1. For resume: `--run --yes` completed → 0, compensated → 0, failed → 1; already concluded → 0; unknown id → 1; confirmation → 1; plan not loaded → 1. For account-issuer: ok → 0, refused → 1. **How a later command enters:** there is no roster. A module under `src/commands` that declares either flag is in the population on the day it lands. `secret rewrap` is the first to have entered that way, and no line names it. The floors are the only hand-edited numbers. They catch a discovery or analyzer that silently returns zero. **Tier:** `unit`, measured with `tierOfFile`: signals `none`. Nothing is spawned and nothing boots. The boot seam is replaced through `vi.mock` and never value-imported. **Cost:** about 23 to 26 s of import at collection on a shared box, outside any clocked window. A single-command file in the same package (`recorded-by.test.ts`) imports in 15.7 s on the same box, so the all-commands discovery adds about 10 s. ## Evidence - **Pin** at `88ae5769c6`: `vitest run --project unit test/json-exit-signal.pin.test.ts` gives 77 passed (77), `VERDICT command-exit 0`. - **Ablations** at `9490dd3d75`, before the merge. Both mutations went through `scripts/ablation-replace.mjs` in a trap-restoring script, and the expected direction was red. - `recorded-by.ts` rethrow replaced. The tool's literal count went 1 → 0 for the anchor and 0 → 1 for the marker, and the blob went `e74172d2` → `31085626`. Result: **5 failed / 71 passed**. The failures are the structural member (5 sites "swallowed by the catch at line 196") and the 4 driven recorded-by cases, which show `stdout carried 2 JSON documents … {"error":"EEXIT: 0","duration":1}`. Restored: blob == HEAD and `git diff HEAD` empty. - `resume.ts` rethrow replaced. Anchor 1 → 0, blob `6e2c7de0` → `2cfe0d4c`. Result: **8 failed / 68 passed** (structural member + all 7 driven resume cases). Restored: blob == HEAD. - The hand `grep -c` of the first marker inside the wrapped command read 0. That marker contains `*`, which grep reads as a regex, so the hand count is void. The tool's literal before/after counts and blob hashes are the evidence that the mutation landed. - **Unit tests that reach the changed commands**, at `88ae5769c6`: the pin, `recorded-by.test.ts`, `multi-value-columns.no-auto-run.test.ts`, `artifact-boot-migration.test.ts`, `format.exit-code.test.ts` and `schema-migration-plugins.test.ts` gave 6 files, 134 tests passed, `VERDICT command-exit 0`. - **Typecheck** at `88ae5769c6`: `pnpm --filter @objectstack/cli typecheck` gave `VERDICT command-exit 0`. `tsc --noEmit` passed. `check:test-typecheck` is OK with its ledger unchanged (3 files / 28 errors / 6 pinned signatures). - **Gates** at `88ae5769c6`: all 65 families from `node scripts/pm/dispatch-gates.mjs --commands` exit 0. `--ran` reconciliation: 65 derived, 65 run, 0 NOT-MEASURED. That zero is derived, because every line carries its exit code. `check:i18n`, `check:i18n-coverage` and `check:i18n-walk-parity` first answered exit 3 (prerequisite: CLI not built) and are green after building their declared closure. `check:dual-build-cjs-loads` first answered exit 3 (6 unrelated packages had no `dist/`) and is green after building them. - **Lint**, as a proven narrowing. ESLint over the 5 changed `.ts` files with `--no-inline-config --format json` read 5 files, 0 errors, 0 warnings. The population comes from ESLint itself: `isPathIgnored` is false for all 5 and `calculateConfigForFile` resolves rules for each. Invariance: `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, no typed rules, stated at line 327), so this diff cannot move the verdict on any untouched file. The full `pnpm lint` is CI's. ## NOT MEASURED - **`packages/cli` full unit tier**, not measured locally. Two attempts at `vitest run --project unit` were cut off: exit 137, then a container restart mid-run. It is narrowed to the 6 unit files above, which import or reach the four changed commands. CI runs the whole tier. - **`packages/cli` integration tier**, declared to CI. The diff touches no integration-tier file and no spawn entry. `artifact-boot-migration.unbuildable-index.test.ts` and `sqlite-occupancy.test.ts` reach the changed commands and live in that tier. - **Nightly e2e** (`json-stdout-purity`, `migrate-exit-code`): not run. They drive only the bare `--json` forms, which never reached the defect. - **Public door, `os migrate account-issuer --json` refusal.** It needs a stack that registers `sys_account` (plugin-auth's object) with colliding rows. The plain project stack does not register it, so the door answers a read refusal before the path is reached. Measured at `88ae5769c6`: `{"error":"Cannot enumerate sys_account: …"}`, one document, exit 1. The driven unit case covers the refusal path. - **Public door, `os migrate resume --run … --yes` completed.** It is unreachable at the door today; see the first acceptance note. The driven unit case covers it. ## Acceptance notes - `os migrate resume --run RUN_ID --yes` cannot resume any run at the public door. `MigrationRecoveryPlugin`, which owns the `migration-plans` registry, is exported from `@objectstack/runtime` but composed nowhere in `packages/cli`. So every interrupted run answers "belongs to plan …, which no loaded package registers … Load the package that owns this migration". That holds even for `metadata.recorded-by-sentinel-to-null`, whose owner `@objectstack/metadata-protocol` the CLI itself loads. `recorded-by`'s in-process `plans.register(plan)` lands in the no-registry `catch` for the same reason. Measured above, and reported to the seat as a separate finding. - On an already-concluded run, `os migrate resume --run RUN_ID --json` exits 0 but puts its message under the `error` key. That is unchanged here, and noted only. - The same `catch` shape sits on three commands with no JSON face: `os package install`, `os package publish` and `os plugin sign`. Measured: `os package install ./does-not-exist.json` prints `✗ Cannot read artifact: …`, then `✗ EEXIT: 1`, and exits 1. The exit status is right and the extra line is wrong. These files are outside this claim's declared file surface, which covers JSON-face commands, so they are not touched here. They are reported to the seat as a finding. --- _Generated by [Claude Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #21326
Clause-②: yes (widening)
Stage 2 of the staged card: the at-rest re-wrap of version-1
sys_secretciphertext, ADR-0128 §4.2, throughrotateKey. Retiring version-1 opening is a later stage and is not here; #21326 stays open for it.⛔ Security family: this body names classes and positions only.
What lands
os secret rewrap, a new subcommand besideos secret orphans, with the same boot, connection, guards and output conventions. It is a dry run by default (a read-only boot that writes nothing), and--applywrites. The other flags are--declared-datasources/--no-declared-datasources,-y, --yes,--jsonand--database-url. Nothing on any boot or upgrade path invokes it, and it has no HTTP surface, so no refusal answers a request and no ADR-0112 ledger row is owed (A8).ciphertextDerivationStatuson@objectstack/service-settings:LocalCryptoProvider's own reading of which AAD derivation sealed a stored ciphertext (current,supersededorunknown). It is read off the marker without opening anything, and it is the samereadCiphertextthatdecryptdispatches on. The re-wrap classifies rows with it, so the CLI never restates the marker grammar.packages/cli/src/utils/sys-secret-rewrap.tsholds the planner and executor (pure planning, injected ports), with its pins, the command's guards test and its concrete-driver contract test.content/docs/deployment/cli.mdxgainsos secret rewrapnext toos secret orphans. The page is hand-written, not generated.bootSchemaStackfamily ledgers:schema-migrate.one-shot-family.integration.test.tsandjson-stdout-purity.e2e.test.ts.minor) and one ADR-0128 anchor for the planner.A1: where version-1 ciphertext is stored (census at
1fd56645af)sys_secret.ciphertextis the only store of ciphertext the provider seals. All three producers write it:SettingsService.set(), the engine's secret-field path and the datasource binder'sbind(). There are no other non-testencryptcall sites. Version 1 means no:marker.sys_setting.value_encinline values are not provider-sealed. They are sealed by the separateCryptoAdapterinterface: noCryptoContext, noICryptoProviderAAD, and its only in-tree implementation is the base64NoopCryptoAdapter. They are not version-1 ciphertext of this provider, sorotateKeycannot reach them and they are out of this class. ⛔ No second sealing path was built.sys_two_factor.backup_codesis not provider-sealed either. It is sealed by better-auth's own symmetric encryption under the auth secret.A2: per-row scope attribution
rotateKey(handle, ctx)seals under the caller's scope, and a version-1 ciphertext binds no scope, so opening one proves nothing about its producer. The scope therefore comes from the holder, through the classification the orphan sweep already uses: the cross-producer reference union. Each union reference already carries its holder family, so the classifier needed no extension. The planner only groups the union's references by handle. ⛔ No second holder walk.SCOPE_OF_HOLDER_FAMILYis aRecordover the closed family set (settings →settings, object-field →object_secret_field, datasource →datasource_credential), so a fourth family stops compiling until it has a scope.The order of the decision:
left_conflicting_scope).left_union_incomplete): a family that was not read may hold it too.--applythen refuses and names the family.left_orphan).A3: the properties, and the pin for each
currentis skipped as done, and there is no run log.--applyis alldonewith the table unchanged.updateManykeyed onidAND the exact ciphertext the run read. All five drivers serve that as a single filtered statement and answer the changed count (measured in source: SQL and TursoUPDATE … WHERE, MongoupdateMany, memory with noawaitbetween filter and write). A count of 0 iswrite_conflict, and the row is not overwritten. A driver withoutupdateManyis refused before any row is opened.write_conflict, the producer's value is kept, and the other rows land. The real SQL driver: a stale ciphertext changes nothing (0), and the read one changes the row (1).refused_unreadable, its bytes unchanged, and the other rows re-wrapped. An unknown marker is never opened.current, carry a usable version, and open under the SAME context to the SAME plaintext, before the write.refused_verify_failed, never written. Positive control: the honest provider re-wraps the same row.--applyproduces. Nothing is written.updateManyis never called.--applyover the same store lands exactly the dry run's counts. The one-shot family pin shows the dry run leaves the database byte-identical, boot included.After
--apply, each re-wrapped row is opened through its producer's own read path: the engine'sresolveSecret, the binder'sresolve, and the settings context. No other producer's context opens it.The key. The run resolves
LocalCryptoProviderbefore the boot, from a key that already exists, in the strict posture with the auto-key opt-in withheld. It hands that provider to the settings service the boot composes, so no provider in the run mints a key. With no key, it hands that service one that refuses every call, and the run refuses before opening a row. Pinned with the real boot in a development posture with no key:crypto_key_unavailable, and no key file appears.A5: output shape
Classes and counts only. ⛔ No plaintext, no ciphertext, no key material, and no row id. The
--jsondocument is{ mode, report }, wherereportholds:modeandkeySource(a source name, never a value);families: per holder family,status,referenceCount, andreasonon a gap;refusal;counts:total,rewrap,done,left,refused,notWritten;byClass: one count for each ofrewrap,done,left_orphan,left_conflicting_scope,left_union_incomplete,refused_unreadable,refused_unknown_derivation,refused_verify_failed,write_conflict,write_failed;rewrapByScope;notes.Refusals are one JSON document with an
errorkey:union_incomplete,driver_cannot_compare_and_set,crypto_key_unavailable,confirmation_required,declared_datasources_unreadable,boot_failed,no_engine,no_sys_secret_driverorscan_failed. A pin asserts that the report holds none of the plaintexts, ciphertexts, row ids or holder coordinates of its fixture.A7: out of this stage, and untouched
These are untouched: version-1 opening and the contract paragraph about it,
packages/spec/src/contracts/crypto-provider.ts(the re-wrap needed no contract change),packages/objectql/src/engine.ts,docs/adr/**, and anything in cloud.Gates and tests (local, at
6a7c27c2f2)Derived gates.
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 107 commands. Each exit code was written to disk before it was read, and all 107 exited 0.--ranreconciles: "✓ dispatch-gates --ran: 107 derived famil(ies) accounted for — 107 run, 0 NOT-MEASURED (a DERIVED zero — all 107 recorded an exit code and none of them is 3)."The gates named in the dispatch:
check-changeset-no-major.mjs --base origin/main: "✓ This diff introduces nomajorbump." The level axis was driven offline, with this body as the event payload: "✓ LEVEL AXIS: this PR declares clause-②yes (widening), and no package whosepackages/**/src/**it moves is gradedpatch." (re-run at219457c029, after the review's fix round)check-adr-0087-registration.mjs --base origin/main: "✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)."check-empty-changeset.mjs --base origin/main: "✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)."pnpm check:changeset-gate-self-tests: exit 0.pnpm check:doc-authoring: "✓ doc authoring guard: sibling-package prose ids hold the baseline — 204 pinned site(s) across 62 file(s) …"pnpm check:nul-bytes: "check-nul-bytes: OK (scanned 9800 text file(s) … no raw ASCII control bytes)."Other gate readings:
check:cli-command-ids: 65 command modules, all of them oclif commands.check:test-source-alias: OK, the unaliased set unchanged.check:engine-double-contract: "OK — 921 pinned".check:adr-anchors: OK, 60 anchored files.check:type-check-debt: OK.check:cross-package-test-inputs: OK.Tests:
@objectstack/service-settings:testpassed 33 files / 605 tests, andtypecheckexited 0.@objectstack/clitypecheck(tsc --noEmitpluscheck:test-typecheck): exit 0.@objectstack/cliunit tier: 246 of 248 files passed on the first run. The other two, thepublished-subpath-*.pinfiles, refused on a missing clidist/(a prerequisite, not a verdict). Once the cli was built, both passed (29 / 29).@objectstack/cliintegration tier, run on the touched files and the secret family (sys-secret-rewrap,rewrap.guards,rewrap.driver-contract,orphans.guards,orphans.driver-contract,sys-secret-orphan-sweep,secret-reference-union): 7 files / 87 tests.--applyruns no seed write, and the family table holds.json-stdout-purity.e2e(nightly tier) was not run here. Its three assertions for the new member were measured by a direct invocation at6a7c27c2f2: stdout is one JSON document (thescan_failedrefusal), no logger record is on stdout, and the three boot diagnostics are on stderr. In that run no database file was created, and the run's key home stayed empty.Lint was narrowed, and the narrowing is declared:
eslint.config.mjs, is**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}.eslint --no-inline-config --format jsonover the 10 changed TS files reported 10 files, 0 errors and 0 warnings.parserOptions.project), so this diff cannot move the verdict on any untouched file.Ablations
Both legs ran from the committed state
6a7c27c2f2, throughscripts/ablation-replace.mjsin WRAP mode, inside the verify lock. Each was restored, and the restore was proven by blob. The expected direction, recorded before running, was RED for both legs.attributeRewrapScopereturned a guessedsettingsscope instead ofleft_orphan.75d607ac5dc0→3c509ef4edb1.left_orphancount ("expected +0 to be 1"). The count pins that include the orphan failed with it: the full apply, resumable, live-safe, fail-closed and report pins.75d607ac5dc0), andgit diff HEADis empty.resealHoldscheck (a 3-line anchor) was deleted.75d607ac5dc0→4fd879b7d1a1.refused_verify_failed("expected +0 to be 1").There is no build leg. The pins import the subject as
./sys-secret-rewrap.js(relative source), so nodist/is on the resolution path. After both legs, the union above is green at6a7c27c2f2.Acceptance notes
os secret orphansmay mint a key file. It composes the settings service with that service's default provider, and in a development posture with no key, that boot mints a key file in the key home. This was measured with this command before it handed the settings service its own provider: a key file appeared in this container's key home during that probe. The report says it writes nothing, which is true of the database. → noted, not filed (承接者:无).Generated by Claude Code